/*
程序调用堆栈捕获与处理工具包

	捕获程序运行时的调用堆栈（即函数调用路径），支持 “仅捕获第一个调用帧” 或 “捕获完整调用堆栈” 两种模式；
	解析堆栈信息为可读格式（包含函数名、文件名、行号），支持自定义格式化输出；
	通过 sync.Pool 复用 Stack 实例，减少频繁创建 / 销毁对象导致的内存开销，优化性能。
	主要用于 错误处理、日志记录、调试诊断 场景 —— 当程序发生错误时，通过捕获调用堆栈，可精准定位错误发生的代码位置（哪个函数、哪个文件、哪一行），大幅降低问题排查难度
*/
package stack

import (
	"fmt"
	"runtime"
	"sync"
)

// Depth 定义堆栈捕获的深度模式，控制捕获调用帧的数量
type Depth int

const (
	// First 仅捕获调用堆栈的第一个帧（即直接触发堆栈捕获的函数位置）
	First Depth = iota

	// Full 捕获完整的调用堆栈（从触发位置到程序入口的所有函数调用路径），
	// 若初始存储不足，会自动扩容以容纳所有帧
	Full
)

// Stack 是调用堆栈的核心结构体，存储捕获到的堆栈信息，
// 包含程序计数器、帧解析器、底层存储，提供堆栈遍历、格式化等方法
type Stack struct {
	pcs     []uintptr       // 存储调用帧的程序计数器（内存地址），每个地址对应一个函数调用
	frames  *runtime.Frames // 帧解析器，用于将pcs转换为可读的帧信息（runtime.Frame） 比如函数名、文件名、代码行号等
	storage []uintptr       // 底层存储切片，用于对象池复用，避免重复分配内存 主要用于“对象池”模式 避免每次捕获堆栈时都重新分配新内存
}

// stacks 是Stack实例的对象池，用于复用Stack对象，减少内存分配与GC压力
// 核心作用：避免每次捕获堆栈时都创建新的Stack实例，尤其适合高频堆栈捕获场景（如循环中的错误处理）
var stacks = sync.Pool{
	// New 是对象池的初始化函数，当池为空时调用，创建一个默认容量的Stack实例
	New: func() interface{} {
		return &Stack{
			storage: make([]uintptr, 64), // 初始分配64个uintptr的底层存储，足够容纳大多数场景的调用帧
		}
	},
}

/*
sync.Pool 的工作原理：
对象池模式：维护一个可重用的对象集合
Get() 方法：从池中获取对象，如果池为空则调用 New 函数
Put(x) 方法：将对象放回池中供后续重用
线程安全：多个 goroutine 可以并发调用 Get/Put
*/

// Callers 是捕获程序调用堆栈的核心函数，从对象池复用Stack实例，
// 根据用户指定的“跳过帧数”和“捕获深度模式”，获取调用堆栈数据并初始化解析所需字段，
// 最终返回可用于后续处理（如获取函数名、文件名）的Stack实例。
// 参数：
//
//	skip: 要跳过的“用户业务层”调用帧数（比如用户封装了错误函数，需要跳过该函数自身的调用帧）
//	depth: 堆栈捕获深度模式（First/Full），控制捕获调用帧的范围
//
// 返回值：
//
//	*Stack: 包含堆栈原始数据和解析迭代器的实例，使用后需调用Free()放回对象池
func Callers(skip int, depth Depth) *Stack {
	// 1. 从对象池获取Stack实例：复用已创建的实例，减少内存分配和GC压力
	// stacks.Get()返回any类型，需强转为*Stack才能操作其内部字段（pcs、storage等）
	stack := stacks.Get().(*Stack)

	// 2. 根据捕获深度模式，初始化pcs切片的初始容量（pcs存储调用帧的内存地址）
	switch depth {
	case First:
		// First模式：仅捕获“直接触发堆栈的1个调用帧”，所以pcs容量设为1
		// 复用stack.storage的前1个元素（避免新创建切片）
		stack.pcs = stack.storage[:1]
	case Full:
		// Full模式：捕获“完整的调用堆栈”，初始用stack.storage的全部容量（默认64）
		// 后续若容量不够会动态扩容
		stack.pcs = stack.storage
	}

	// 3. 调用标准库runtime.Callers，实际捕获调用堆栈
	// skip+2：跳过无关调用帧——+2是固定值，分别跳过当前Callers函数、runtime.Callers标准库函数
	// stack.pcs：存储捕获到的“程序计数器（函数内存地址）”，numFrames是实际捕获到的帧数
	numFrames := runtime.Callers(skip+2, stack.pcs)

	// 4. 针对Full模式：若初始容量不够，动态扩容直到能装下所有调用帧
	if depth == Full {
		// 用临时变量pcs引用当前stack.pcs，避免直接修改stack.pcs影响后续
		pcs := stack.pcs
		// 循环条件：实际捕获的帧数 == 当前切片容量 → 说明切片满了，还有帧没捕获到
		for numFrames == len(pcs) {
			// 扩容：新切片长度是原切片的2倍（平衡性能和内存，避免频繁扩容）
			pcs = make([]uintptr, len(pcs)*2)
			// 用扩容后的切片重新捕获堆栈，更新实际帧数
			numFrames = runtime.Callers(skip+2, pcs)
		}
		// 截取有效部分：只保留存储了调用帧的部分（去掉未使用的空间）
		stack.pcs = pcs[:numFrames]
		// 更新底层存储：把扩容后的切片赋值给stack.storage，下次复用该实例时可直接用大容量，减少再次扩容
		stack.storage = pcs
	} else {
		// 针对First模式：直接截取有效部分（最多1个帧），避免无效数据
		stack.pcs = stack.pcs[:numFrames]
	}

	// 5. 初始化堆栈解析迭代器：把原始的“程序计数器切片”转成可解析的Frames迭代器
	// 后续通过stack.Next()或stack.Frames()就能获取函数名、文件名等可读信息
	stack.frames = runtime.CallersFrames(stack.pcs)

	// 6. 返回初始化完成的Stack实例，用户可通过该实例获取堆栈详情
	return stack
}

/*
两种模式的核心区别是 “要捕获多少层调用帧”，可以类比成 “查问题时看的范围大小”，下面用实际开发场景举例说明：
1. First 模式：“只看直接出错的那一步”
通俗定义：只捕获 “最靠近问题” 的 1 个调用帧（比如直接触发错误的函数），不关心这个函数是被哪个上层函数调用的。
核心特点：轻量、速度快，只占 1 个帧的内存，适合 “快速定位直接原因”。
举例场景：
假设你写了一个用户登录功能，调用链是：
用户点击登录按钮 → Login接口函数 → CheckPassword函数（这里出错了）
用 First 模式捕获堆栈时，只会抓到 CheckPassword 函数的位置（比如user.go:30行），不会管它是被 Login 调用的 —— 适合你已经知道 “是密码校验出问题”，只需要快速找到校验函数在哪行的场景。
2. Full 模式：“看完整的调用链条”
通俗定义：捕获从 “问题函数” 到 “最上层调用函数” 的所有调用帧，能看到完整的 “谁调用了谁”，就像查快递物流记录一样，能看到每一步的流转。
核心特点：完整、信息全，会动态扩容确保不遗漏任何一层，但内存占用比 First 模式多，适合 “排查深层原因”。
举例场景：
还是上面的登录场景，用 Full 模式捕获堆栈时，会抓到 Login 函数（调用者）→ CheckPassword 函数（出错者） 的完整链条，甚至能看到 Login 是被哪个 HTTP 处理函数调用的 —— 适合你不知道 “为什么会走到 CheckPassword 函数” 的场景，比如排查 “是不是有其他非登录的接口误调用了密码校验，导致参数传错”
*/

// Free 将Stack实例放回对象池，重置内部字段，供后续复用
func (st *Stack) Free() { // 定义 Stack 的方法 Free，接收者为指针（*Stack），无参数和返回值
	st.pcs = nil    // 重置 pcs 为 nil（释放引用，避免残留数据）
	st.frames = nil // 重置 frames 为 nil
	stacks.Put(st)  // 将实例放回对象池（供下次 Get() 复用）
}

// Next 获取堆栈中的下一个调用帧（runtime.Frame）
func (st *Stack) Next() (fr runtime.Frame, more bool) { // 方法 Next 返回当前帧和是否有更多帧的标志
	return st.frames.Next() // 调用 frames.Next() 获取下一个帧（runtime.Frame 包含函数名、文件等信息）
}

// Frames 将完整堆栈解析为[]runtime.Frame切片，包含所有可读的调用帧信息
func (st *Stack) Frames() []runtime.Frame { // 方法 Frames 返回所有帧的切片
	frames := make([]runtime.Frame, 0, len(st.pcs)) // 创建容量为 len(st.pcs) 的切片（预分配，优化性能）
	frame, more := st.Next()                        // 获取第一个帧
	frames = append(frames, frame)                  // 将第一个帧加入切片
	for more {                                      // 循环获取剩余帧，直到没有更多（more 为 false）
		if frame, more = st.Next(); more { // 获取下一个帧
			frames = append(frames, frame) // 加入切片
		}
	}
	return frames // 返回包含所有帧的切片
}

// String 实现stringer接口，返回堆栈的字符串表示（调用Format方法）
func (st *Stack) String() string { // 实现 fmt.Stringer 接口，返回字符串
	return fmt.Sprintf("%s", st) // 调用 fmt.Sprintf，通过 %s 格式化 st（触发 Format 方法）
}

// Format 实现 Go 标准库的 fmt.Formatter 接口，为 Stack 实例提供自定义格式化输出逻辑，
// 支持通过 fmt 包函数（如 fmt.Printf、fmt.Println）按指定格式打印堆栈信息，
// 解决默认打印结构体时信息杂乱的问题，输出结构化的“调用帧索引+函数名+文件路径+行号”，便于调试和日志分析。
//
// 参数说明：
//
//	s: fmt.State 类型，是格式化输出的状态对象，主要提供两个核心能力：
//	   1. 写入输出内容：通过 io.Writer 接口的 Write 方法（fmt.State 继承 io.Writer）将格式化后的字符串写入目标（如控制台、日志文件）；
//	   2. 判断格式化标志：支持通过 s.Flag('+') 等判断是否携带额外格式化标志（当前方法暂未使用标志判断，仅处理基础格式）。
//	verb: rune 类型，表示当前使用的格式化动词（如 '%s'、'%v'），
//	      该方法仅处理两种常用动词，其他动词（如 '%d'、'%x'）会触发 fmt 包的默认格式化逻辑（打印结构体地址等无意义信息）。
//
// 返回值说明：
//
//	无显式返回值，格式化后的内容通过 s.Write 写入输出目标，错误会通过 fmt 包内部机制处理（如写入失败时返回错误）。
//
// 支持的格式化动词及输出格式：
//  1. '%s'：按“精简结构化格式”输出，每个调用帧占两行，第一行是“索引). 函数名”，第二行是“\t文件路径:行号”（\t 用于缩进对齐，提升可读性）；
//  2. '%v'：与 '%s' 输出格式完全一致（当前设计为简化使用，避免用户记忆多格式，后续可根据需求扩展为不同细节程度）。
//
// 内部逻辑步骤：
//  1. 动词判断：通过 switch 语句筛选需要处理的格式化动词（仅 '%s' 和 '%v'），其他动词不进入自定义逻辑，交由 fmt 包默认处理；
//  2. 获取调用帧列表：调用当前 Stack 实例的 Frames() 方法，获取所有已解析的调用帧切片（[]runtime.Frame），
//     每个 runtime.Frame 包含函数名（Function）、文件路径（File）、行号（Line）等核心信息；
//  3. 循环遍历输出：通过 for 循环遍历每个调用帧，使用 fmt.Fprintf 按固定格式写入输出目标：
//     - i+1：将切片索引（从 0 开始）转为用户习惯的“从 1 开始的索引”，便于区分不同调用帧的顺序；
//     - f.Function：输出函数的完整路径（如 "main/login.checkPwd"），明确调用的具体函数；
//     - f.File：输出函数所在的文件路径（如 "/home/project/main/login.go"），定位代码文件；
//     - f.Line：输出函数调用所在的行号（如 35），精准定位代码位置；
//     - '\n' 和 '\t'：通过换行和缩进优化输出结构，避免信息堆砌，提升阅读体验。
//
// 注意事项：
//  1. 若 Stack 实例未捕获到任何调用帧（st.Frames() 返回空切片），该方法会输出空内容，无报错；
//  2. 若需扩展其他格式化动词（如 '%+v' 输出更详细的堆栈信息，包含函数参数等），可在 switch 中新增 case 并补充格式化逻辑；
//  3. 该方法的输出内容会跟随 Frames() 方法的过滤逻辑（如 Frames() 若过滤了 runtime.main 帧，此处也不会输出该帧）。
func (st *Stack) Format(s fmt.State, verb rune) {
	// 仅处理 '%s' 和 '%v' 两种格式化动词，其他动词交由 fmt 包默认处理
	switch verb {
	case 's', 'v': // 这个语法就go支持，其他语言必须另外一个case
		// 1. 调用 Frames() 方法，获取所有已解析的调用帧切片（包含函数名、文件、行号等可读信息）
		allFrames := st.Frames()
		// 2. 循环遍历每个调用帧，按结构化格式输出
		for frameIndex, frame := range allFrames {
			// 使用 fmt.Fprintf 将格式化内容写入 s（fmt.State），最终输出到目标（如控制台）
			// 格式说明：
			// - %d). ：调用帧索引（从 1 开始），加 ). 便于区分序号和内容；
			// - %s ：函数完整路径（如 "main/login.checkPwd"）；
			// - \n\t ：换行后缩进，使文件和行号与函数名错开，提升可读性；
			// - %s:%d ：文件路径 + 行号（如 "/home/project/main/login.go:35"），精准定位代码位置。
			fmt.Fprintf(s, "%d). %s\n\t%s:%d\n",
				frameIndex+1,   // 切片索引从 0 开始，转为用户习惯的 1 开始序号
				frame.Function, // 当前调用帧对应的函数完整名称（包含包路径）
				frame.File,     // 当前调用帧所在的代码文件路径（绝对路径或相对路径，取决于运行环境）
				frame.Line,     // 当前调用帧对应的代码行号（整数）
			)
		}
		// 这里把所有内容都打印出来了，总的来说这个栈帧在go里面还是没那么好打印
	}
}
